diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f4692041f6..5b66531a7b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -21,6 +21,7 @@ Changelog * Add simple admin keyboard shortcuts overview dialog, available in the help sub-menu (Karthik Ayangar, Rohit Sharma) * Add ability to bulk toggle permissions in the user group editing view, including shift+click for multiple selections (LB (Ben) Johnston, Kalob Taulien) * Update the minimum version of `djangorestframework` to 3.15.1 (Sage Abdullah) + * Add support for related fields in generic `IndexView.list_display` (Abdelrahman Hamada) * Fix: Fix typo in `__str__` for MySQL search index (Jake Howard) * Fix: Ensure that unit tests correctly check for migrations in all core Wagtail apps (Matt Westcott) * Fix: Correctly handle `date` objects on `human_readable_date` template tag (Jhonatan Lopes) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dd2545aab9..7685ac8cd6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -808,6 +808,7 @@ * Mark Niehues * Georgios Roumeliotis * David Buxton +* Abdelrahman Hamada ## Translators diff --git a/docs/releases/6.1.md b/docs/releases/6.1.md index d451756c3a..888bc366c9 100644 --- a/docs/releases/6.1.md +++ b/docs/releases/6.1.md @@ -31,6 +31,7 @@ depth: 1 * Add simple admin keyboard shortcuts overview dialog, available in the help sub-menu (Karthik Ayangar, Rohit Sharma) * Add ability to bulk toggle permissions in the user group editing view, including shift+click for multiple selections (LB (Ben) Johnston, Kalob Taulien) * Update the minimum version of `djangorestframework` to 3.15.1 (Sage Abdullah) + * Add support for related fields in generic `IndexView.list_display` (Abdelrahman Hamada) ### Bug fixes diff --git a/wagtail/admin/ui/tables/__init__.py b/wagtail/admin/ui/tables/__init__.py index 36a1018dfd..474f9d7c1b 100644 --- a/wagtail/admin/ui/tables/__init__.py +++ b/wagtail/admin/ui/tables/__init__.py @@ -149,7 +149,10 @@ class Column(BaseColumn): if callable(self.accessor): return self.accessor(instance) else: - return multigetattr(instance, self.accessor) + try: + return multigetattr(instance, self.accessor) + except AttributeError: + return None def get_cell_context_data(self, instance, parent_context): context = super().get_cell_context_data(instance, parent_context) diff --git a/wagtail/admin/views/generic/models.py b/wagtail/admin/views/generic/models.py index 04fea7a002..5f646e24ee 100644 --- a/wagtail/admin/views/generic/models.py +++ b/wagtail/admin/views/generic/models.py @@ -9,6 +9,7 @@ from django.core.exceptions import ( ) from django.db import models, transaction from django.db.models import Q +from django.db.models.constants import LOOKUP_SEP from django.db.models.functions import Cast from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect @@ -256,7 +257,32 @@ class IndexView( ) def _get_custom_column(self, field_name, column_class=Column, **kwargs): - label, attr = label_for_field(field_name, self.model, return_attr=True) + lookups = ( + [field_name] + if hasattr(self.model, field_name) + else field_name.split(LOOKUP_SEP) + ) + *relations, field = lookups + model_class = self.model + + # Iterate over the relation list to try to get the last model + # where the field exists + foreign_field_name = "" + for model in relations: + foreign_field = model_class._meta.get_field(model) + foreign_field_name = foreign_field.verbose_name + model_class = foreign_field.related_model + + label, attr = label_for_field(field, model_class, return_attr=True) + + # For some languages, it may be more appropriate to put the field label + # before the related model name + if foreign_field_name: + label = _("%(related_model_name)s %(field_label)s") % { + "related_model_name": foreign_field_name, + "field_label": label, + } + sort_key = getattr(attr, "admin_order_field", None) # attr is None if the field is an actual database field, @@ -264,8 +290,12 @@ class IndexView( if attr is None: sort_key = field_name + accessor = field_name + # Build the dotted relation if needed, for use in multigetattr + if relations: + accessor = ".".join(lookups) return column_class( - field_name, + accessor, label=capfirst(label), sort_key=sort_key, **kwargs, diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py index 9cce127184..8e99b6e7a9 100644 --- a/wagtail/snippets/tests/test_viewset.py +++ b/wagtail/snippets/tests/test_viewset.py @@ -42,6 +42,7 @@ from wagtail.test.testapp.models import ( ) from wagtail.test.utils import WagtailTestUtils from wagtail.test.utils.template_tests import AdminTemplateTestUtils +from wagtail.utils.timestamps import render_timestamp class TestIncorrectRegistration(SimpleTestCase): @@ -775,6 +776,44 @@ class TestListViewWithCustomColumns(BaseSnippetViewSetTests): ) +class TestRelatedFieldListDisplay(BaseSnippetViewSetTests): + model = SnippetChooserModel + + def setUp(self): + super().setUp() + url = "https://example.com/free_examples" + self.advert = Advert.objects.create(url=url, text="Free Examples") + self.ffs = FullFeaturedSnippet.objects.create(text="royale with cheese") + + def test_empty_foreignkey(self): + self.no_ffs_chooser = self.model.objects.create(advert=self.advert) + response = self.client.get(self.get_url("list")) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Chosen snippet text") + self.assertContains(response, "