0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-29 09:33:54 +01:00

Allow customising snippet listing columns with list_display

This commit is contained in:
Sage Abdullah 2022-09-27 12:58:11 +01:00 committed by Matt Westcott
parent 5e2a827afa
commit 0a0bd30fad
4 changed files with 94 additions and 43 deletions

View File

@ -77,6 +77,7 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit
```{eval-rst}
.. autoclass:: wagtail.snippets.views.snippets.SnippetViewSet
.. autoattribute:: list_display
.. autoattribute:: filterset_class
.. autoattribute:: index_view_class
.. autoattribute:: add_view_class

View File

@ -323,7 +323,7 @@ You can also save revisions programmatically by calling the {meth}`~wagtail.mode
The `DraftStateMixin` class was introduced.
```
If a snippet model inherits from {class}`~wagtail.models.DraftStateMixin`, Wagtail will automatically change the "Save" action menu in the snippets admin to "Save draft" and add a new "Publish" action menu. Any changes you save in the snippets admin will be saved as revisions and will not be reflected to the "live" snippet instance until you publish the changes. For example, the `Advert` snippet could save draft changes by defining it as follows:
If a snippet model inherits from {class}`~wagtail.models.DraftStateMixin`, Wagtail will automatically add a live/draft status column to the listing view, change the "Save" action menu to "Save draft", and add a new "Publish" action menu in the editor. Any changes you save in the snippets admin will be saved as revisions and will not be reflected to the "live" snippet instance until you publish the changes. For example, the `Advert` snippet could save draft changes by defining it as follows:
```python
# ...
@ -400,7 +400,7 @@ This can be done by removing the `@register_snippet` decorator on your model cla
register_snippet(MyModel, viewset=MyModelViewSet)
```
For example, with the following `Member` model:
For example, with the following `Member` model and a `MemberFilterSet` class:
```python
# models.py
@ -418,6 +418,12 @@ class Member(models.Model):
name = models.CharField(max_length=255)
shirt_size = models.CharField(max_length=5, choices=ShirtSize.choices, default=ShirtSize.MEDIUM)
def get_shirt_size_display(self):
return self.ShirtSize(self.shirt_size).label
get_shirt_size_display.admin_order_field = "shirt_size"
get_shirt_size_display.short_description = "Size description"
class MemberFilterSet(WagtailFilterSet):
class Meta:
@ -425,16 +431,18 @@ class MemberFilterSet(WagtailFilterSet):
fields = ["shirt_size"]
```
You can add a `filterset_class` to the listing view by defining a subclass of `SnippetViewSet` as below:
You can define a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.list_display` attribute to specify the columns shown on the listing view. You can also add the ability to filter the listing view by defining a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.filterset_class` attribute on a subclass of `SnippetViewSet`. For example:
```python
# views.py
from wagtail.admin.ui.tables import UpdatedAtColumn
from wagtail.snippets.views.snippets import SnippetViewSet
from myapp.models import MemberFilterSet
class MemberViewSet(SnippetViewSet):
list_display = ["name", "shirt_size", "get_shirt_size_display", UpdatedAtColumn()]
filterset_class = MemberFilterSet
```

View File

@ -1,5 +1,5 @@
from django import VERSION as DJANGO_VERSION
from django.contrib.admin.utils import quote, unquote
from django.contrib.admin.utils import label_for_field, quote, unquote
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.db import models, transaction
@ -22,7 +22,7 @@ from wagtail.admin import messages
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.panels import get_edit_handler
from wagtail.admin.templatetags.wagtailadmin_tags import user_display_name
from wagtail.admin.ui.tables import DateColumn, StatusTagColumn, Table, TitleColumn
from wagtail.admin.ui.tables import Column, Table, TitleColumn, UpdatedAtColumn
from wagtail.log_actions import log
from wagtail.log_actions import registry as log_registry
from wagtail.models import DraftStateMixin, RevisionMixin
@ -86,6 +86,7 @@ class IndexView(
filters = None
filterset_class = None
table_class = Table
list_display = ["__str__", UpdatedAtColumn()]
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
@ -241,48 +242,49 @@ class IndexView(
}
return queryset.filter(**filters)
def _get_title_column(self, column_class=TitleColumn):
def title_accessor(obj):
draftstate_enabled = self.model and issubclass(self.model, DraftStateMixin)
if draftstate_enabled and obj.latest_revision:
return obj.latest_revision.object_str
return str(obj)
return column_class(
"name",
label=gettext_lazy("Name"),
accessor=title_accessor,
get_url=self.get_edit_url,
def _get_title_column(self, field_name, column_class=TitleColumn, **kwargs):
if not self.model:
return column_class(
"name",
label=gettext_lazy("Name"),
accessor=str,
get_url=self.get_edit_url,
)
return self._get_custom_column(
field_name, column_class, get_url=self.get_edit_url, **kwargs
)
def _get_updated_at_column(self, column_class=DateColumn):
return column_class("_updated_at", label=_("Updated"), sort_key="_updated_at")
def _get_custom_column(self, field_name, column_class=Column, **kwargs):
label, attr = label_for_field(field_name, self.model, return_attr=True)
sort_key = getattr(attr, "admin_order_field", None)
# attr is None if the field is an actual database field,
# so it's possible to sort by it
if attr is None:
sort_key = field_name
def _get_status_tag_column(self, column_class=StatusTagColumn):
return column_class(
"status_string",
label=_("Status"),
sort_key="live",
primary=lambda instance: instance.live,
field_name,
label=label.title(),
sort_key=sort_key,
**kwargs,
)
def _get_default_columns(self):
columns = [
self._get_title_column(),
self._get_updated_at_column(),
]
draftstate_enabled = self.model and issubclass(self.model, DraftStateMixin)
if draftstate_enabled:
columns.append(self._get_status_tag_column())
return columns
def get_columns(self):
try:
return self.columns
except AttributeError:
return self._get_default_columns()
columns = []
for i, field in enumerate(self.list_display):
if isinstance(field, Column):
column = field
elif i == 0:
column = self._get_title_column(field)
else:
column = self._get_custom_column(field)
columns.append(column)
return columns
def get_index_url(self):
if self.index_url_name:

View File

@ -23,6 +23,7 @@ from wagtail.admin.ui.tables import (
Column,
DateColumn,
InlineActionsTable,
LiveStatusTagColumn,
TitleColumn,
UserColumn,
)
@ -158,8 +159,19 @@ class IndexView(generic.IndexView):
results_only = False
table_class = InlineActionsTable
def _get_title_column(self, column_class=SnippetTitleColumn):
return super()._get_title_column(column_class)
def _get_title_column(self, field_name, column_class=SnippetTitleColumn, **kwargs):
accessor = kwargs.pop("accessor", None)
if not accessor and field_name == "__str__":
def accessor(obj):
if isinstance(obj, DraftStateMixin) and obj.latest_revision:
return obj.latest_revision.object_str
return str(obj)
return super()._get_title_column(
field_name, column_class, accessor=accessor, **kwargs
)
def get_columns(self):
return [
@ -680,6 +692,21 @@ class SnippetViewSet(ViewSet):
#: A subclass of ``wagtail.admin.filters.WagtailFilterSet``, which is a subclass of `django_filters.FilterSet <https://django-filter.readthedocs.io/en/stable/ref/filterset.html>`_. This will be passed to the ``filterset_class`` attribute of the index view.
filterset_class = None
#: A list or tuple, where each item is either:
#:
#: - The name of a field on the model;
#: - The name of a callable or property on the model that accepts a single parameter for the model instance; or
#: - An instance of the ``wagtail.admin.ui.tables.Column`` class.
#:
#: If the name refers to a database field, the ability to sort the listing by the database column will be offerred and the field's verbose name will be used as the column header.
#:
#: If the name refers to a callable or property, a ``admin_order_field`` attribute can be defined on it to point to the database column for sorting.
#: A ``short_description`` attribute can also be defined on the callable or property to be used as the column header.
#:
#: This list will be passed to the ``list_display`` attribute of the index view.
#: If left unset, the ``list_display`` attribute of the index view will be used instead, which by default is defined as ``["__str__", wagtail.admin.ui.tables.UpdatedAtColumn()]``.
list_display = None
#: The view class to use for the index view; must be a subclass of ``wagtail.snippet.views.snippets.IndexView``.
index_view_class = IndexView
@ -713,6 +740,17 @@ class SnippetViewSet(ViewSet):
#: The view class to use for previewing on the edit view; must be a subclass of ``wagtail.snippet.views.snippets.PreviewOnEditView``.
preview_on_edit_view_class = PreviewOnEditView
def __init__(self, name, **kwargs):
super().__init__(name, **kwargs)
self.preview_enabled = issubclass(self.model, PreviewableMixin)
self.revision_enabled = issubclass(self.model, RevisionMixin)
self.draftstate_enabled = issubclass(self.model, DraftStateMixin)
if not self.list_display:
self.list_display = self.index_view_class.list_display.copy()
if self.draftstate_enabled:
self.list_display += [LiveStatusTagColumn()]
@property
def revisions_revert_view_class(self):
"""
@ -745,6 +783,7 @@ class SnippetViewSet(ViewSet):
add_url_name=self.get_url_name("add"),
edit_url_name=self.get_url_name("edit"),
delete_multiple_url_name=self.get_url_name("delete-multiple"),
list_display=self.list_display,
)
@property
@ -759,6 +798,7 @@ class SnippetViewSet(ViewSet):
add_url_name=self.get_url_name("add"),
edit_url_name=self.get_url_name("edit"),
delete_multiple_url_name=self.get_url_name("delete-multiple"),
list_display=self.list_display,
)
@property
@ -896,7 +936,7 @@ class SnippetViewSet(ViewSet):
path("history/<str:pk>/", self.history_view, name="history"),
]
if issubclass(self.model, PreviewableMixin):
if self.preview_enabled:
urlpatterns += [
path("preview/", self.preview_on_add_view, name="preview_on_add"),
path(
@ -906,8 +946,8 @@ class SnippetViewSet(ViewSet):
),
]
if issubclass(self.model, RevisionMixin):
if issubclass(self.model, PreviewableMixin):
if self.revision_enabled:
if self.preview_enabled:
urlpatterns += [
path(
"history/<str:pk>/revisions/<int:revision_id>/view/",
@ -929,7 +969,7 @@ class SnippetViewSet(ViewSet):
),
]
if issubclass(self.model, DraftStateMixin):
if self.draftstate_enabled:
urlpatterns += [
path("unpublish/<str:pk>/", self.unpublish_view, name="unpublish"),
]