Add support for related fields in generic IndexView.list_display

pull/11588/head
Abdelrahman 2024-02-05 02:58:25 +02:00 zatwierdzone przez Sage Abdullah
rodzic d8085c6ee6
commit 0599a56d81
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: EB1A33CC51CC0217
9 zmienionych plików z 97 dodań i 4 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -808,6 +808,7 @@
* Mark Niehues
* Georgios Roumeliotis
* David Buxton
* Abdelrahman Hamada
## Translators

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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,

Wyświetl plik

@ -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, "<td></td>", html=True)
def test_single_level_relation(self):
self.scm = self.model.objects.create(advert=self.advert, full_featured=self.ffs)
response = self.client.get(self.get_url("list"))
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
headers = [
header.get_text(strip=True)
for header in soup.select("#listing-results table th")
]
self.assertIn("Chosen snippet text", headers)
self.assertContains(response, "<td>royale with cheese</td>", html=True)
def test_multi_level_relation(self):
self.scm = self.model.objects.create(advert=self.advert, full_featured=self.ffs)
dummy_revision = self.ffs.save_revision()
timestamp = render_timestamp(dummy_revision.created_at)
response = self.client.get(self.get_url("list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Latest revision created at")
self.assertContains(response, f"<td>{timestamp}</td>", html=True)
class TestListExport(BaseSnippetViewSetTests):
model = FullFeaturedSnippet

Wyświetl plik

@ -19,6 +19,7 @@ class Migration(migrations.Migration):
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="tests.fullfeaturedsnippet",
verbose_name="Chosen snippet",
),
),
]

Wyświetl plik

@ -1434,7 +1434,11 @@ class EventPageChooserModel(models.Model):
class SnippetChooserModel(models.Model):
advert = models.ForeignKey(Advert, help_text="help text", on_delete=models.CASCADE)
full_featured = models.ForeignKey(
FullFeaturedSnippet, on_delete=models.CASCADE, null=True, blank=True
FullFeaturedSnippet,
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name="Chosen snippet",
)
panels = [

Wyświetl plik

@ -31,6 +31,7 @@ from wagtail.test.testapp.models import (
ModeratedModel,
RevisableChildModel,
RevisableModel,
SnippetChooserModel,
VariousOnDeleteModel,
)
from wagtail.test.testapp.views import (
@ -385,12 +386,24 @@ class VariousOnDeleteModelViewSet(SnippetViewSet):
inspect_view_enabled = True
class SnippetChooserModelViewSet(SnippetViewSet):
model = SnippetChooserModel
list_display = [
"__str__",
"full_featured__text",
"full_featured__latest_revision__created_at",
]
exclude_form_fields = []
register_snippet(FullFeaturedSnippet, viewset=FullFeaturedSnippetViewSet)
register_snippet(DraftStateModel, viewset=DraftStateModelViewSet)
# Works with both classes and instances
register_snippet(ModeratedModelViewSet())
register_snippet(RevisableViewSetGroup)
register_snippet(VariousOnDeleteModelViewSet)
register_snippet(SnippetChooserModelViewSet)
@hooks.register("register_bulk_action")