Change list_display logic

This commit is contained in:
zandercymatics 2023-12-18 10:59:12 -07:00
parent 43a595081f
commit e5e5fc66e6
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
2 changed files with 46 additions and 26 deletions

View file

@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
class MultiFieldSortableChangeList(admin.views.main.ChangeList): class MultiFieldSortableChangeList(admin.views.main.ChangeList):
""" """
This class overrides the behavior of column sorting in django admin tables in order This class overrides the behavior of column sorting in django admin tables in order
to allow multi field sorting to allow for multi field sorting on admin_order_field
Usage: Usage:
@ -46,11 +46,9 @@ class MultiFieldSortableChangeList(admin.views.main.ChangeList):
def get_ordering(self, request, queryset): def get_ordering(self, request, queryset):
""" """
Returns the list of ordering fields for the change list. Returns the list of ordering fields for the change list.
First we check the get_ordering() method in model admin, then we check
the object's default ordering. Then, any manually-specified ordering Mostly identical to the base implementation, except that now it can return
from the query string overrides anything. Finally, a deterministic a list of order_field objects rather than just one.
order is guaranteed by ensuring the primary key is used as the last
ordering field.
""" """
params = self.params params = self.params
ordering = list(self.model_admin.get_ordering(request) or self._get_default_ordering()) ordering = list(self.model_admin.get_ordering(request) or self._get_default_ordering())
@ -74,7 +72,7 @@ class MultiFieldSortableChangeList(admin.views.main.ChangeList):
else: else:
ordering.append(pfx + order_fields) ordering.append(pfx + order_fields)
except (IndexError, ValueError) as err: except (IndexError, ValueError):
continue # Invalid ordering specified, skip it. continue # Invalid ordering specified, skip it.
# Add the given query's ordering fields, if any. # Add the given query's ordering fields, if any.
@ -423,11 +421,10 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
_meta = Meta() _meta = Meta()
# TODO - maybe instead of get we just call it "sort"?
# Columns # Columns
list_display = [ list_display = [
"get_user", "user",
"get_domain", "domain",
"role", "role",
] ]
@ -497,14 +494,12 @@ class DomainInvitationAdmin(ListHeaderAdmin):
class DomainInformationAdmin(ListHeaderAdmin): class DomainInformationAdmin(ListHeaderAdmin):
"""Customize domain information admin class.""" """Customize domain information admin class."""
# TODO - include the orderable fk fields inside list display
# Columns # Columns
list_display = [ list_display = [
"get_domain", "domain",
"organization_type", "organization_type",
"created_at", "created_at",
"get_submitter", "submitter",
] ]
orderable_fk_fields = [ orderable_fk_fields = [
@ -643,12 +638,12 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Columns # Columns
list_display = [ list_display = [
"get_requested_domain", "requested_domain",
"status", "status",
"organization_type", "organization_type",
"created_at", "created_at",
"get_submitter", "submitter",
"get_investigator", "investigator",
] ]
orderable_fk_fields = [ orderable_fk_fields = [

View file

@ -27,17 +27,37 @@ class OrderableFieldsMixin:
It dynamically adds a new method to the class for each field in `orderable_fk_fields`. It dynamically adds a new method to the class for each field in `orderable_fk_fields`.
""" """
new_class = super().__new__(cls) new_class = super().__new__(cls)
# Check if the list_display attribute exists, and if it does, create a local copy of that list.
list_display_exists = hasattr(cls, "list_display")
new_list_display = []
if list_display_exists and isinstance(cls.list_display, list):
new_list_display = cls.list_display.copy()
for field, sort_field in cls.orderable_fk_fields: for field, sort_field in cls.orderable_fk_fields:
setattr(new_class, f"get_{field}", cls._create_orderable_field_method(field, sort_field)) updated_name = f"get_{field}"
# For each item in orderable_fk_fields, create a function and associate it with admin_order_field.
setattr(new_class, updated_name, cls._create_orderable_field_method(field, sort_field))
# Update the list_display variable to use our newly created functions
if list_display_exists and field in cls.list_display:
index = new_list_display.index(field)
new_list_display[index] = updated_name
elif list_display_exists:
new_list_display.append(updated_name)
# Replace the old list with the updated one
if list_display_exists:
cls.list_display = new_list_display
return new_class return new_class
@classmethod @classmethod
def _create_orderable_field_method(cls, field, sort_field): def _create_orderable_field_method(cls, field, sort_field):
""" """
This class method is a factory for creating dynamic methods that will be attached to the ModelAdmin subclass. This class method is a factory for creating dynamic methods that will be attached to the ModelAdmin subclass.
It is used to customize how fk fields are ordered. By default, fks are ordered by id, so if you wish to It is used to customize how fk fields are ordered.
order by "name" instead, you need to manually override that.
In essence, this function will more or less generate code that looks like this, In essence, this function will more or less generate code that looks like this,
for a given tuple defined in orderable_fk_fields: for a given tuple defined in orderable_fk_fields:
@ -47,7 +67,7 @@ class OrderableFieldsMixin:
return obj.requested_domain return obj.requested_domain
# Allows column order sorting # Allows column order sorting
get_requested_domain.admin_order_field = "requested_domain__name" get_requested_domain.admin_order_field = "requested_domain__name"
# Sets column's header # Sets column's header name
get_requested_domain.short_description = "requested domain" get_requested_domain.short_description = "requested domain"
``` ```
@ -65,8 +85,7 @@ class OrderableFieldsMixin:
Parameters: Parameters:
cls: The class that this method is being called on. In the context of this mixin, it would be the ModelAdmin subclass. cls: The class that this method is being called on. In the context of this mixin, it would be the ModelAdmin subclass.
field: A string representing the name of the attribute that the dynamic method will fetch from the model instance. field: A string representing the name of the attribute that the dynamic method will fetch from the model instance.
sort_field: A string or list of strings representing the field(s) sort_field: A string or list of strings representing the field(s) to sort by (ex: "name" or "creator")
that Django should sort by when the column is clicked in the admin interface.
Returns: Returns:
method: The dynamically created method. method: The dynamically created method.
@ -78,23 +97,29 @@ class OrderableFieldsMixin:
""" """
def method(obj): def method(obj):
""" """
The dynamically created method. Method factory.
When called on a model instance, it returns the value of the specified attribute.
""" """
attr = getattr(obj, field) attr = getattr(obj, field)
return attr return attr
# Set the function name. For instance, if the field is "domain",
# then this will generate a function called "get_domain"
method.__name__ = f"get_{field}" method.__name__ = f"get_{field}"
# Check if a list is passed in, or just a string.
if isinstance(sort_field, list): if isinstance(sort_field, list):
sort_list = [] sort_list = []
for sort_field_item in sort_field: for sort_field_item in sort_field:
order_field_string = f"{field}__{sort_field_item}" order_field_string = f"{field}__{sort_field_item}"
sort_list.append(order_field_string) sort_list.append(order_field_string)
# If its a list, return an array of fields to sort on.
# For instance, ["creator__first_name", "creator__last_name"]
method.admin_order_field = sort_list method.admin_order_field = sort_list
else: else:
# If its not a list, just return a string
method.admin_order_field = f"{field}__{sort_field}" method.admin_order_field = f"{field}__{sort_field}"
# Infer the column name in a similar manner to how Django does
method.short_description = field.replace("_", " ") method.short_description = field.replace("_", " ")
return method return method