kopia lustrzana https://github.com/wagtail/wagtail
Add support for related fields in generic IndexView.list_display
rodzic
d8085c6ee6
commit
0599a56d81
|
@ -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)
|
||||
|
|
|
@ -808,6 +808,7 @@
|
|||
* Mark Niehues
|
||||
* Georgios Roumeliotis
|
||||
* David Buxton
|
||||
* Abdelrahman Hamada
|
||||
|
||||
## Translators
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
|||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tests.fullfeaturedsnippet",
|
||||
verbose_name="Chosen snippet",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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")
|
||||
|
|
Ładowanie…
Reference in New Issue