From 25e73797b51f66ce8ec490d53f405bd672fc1d96 Mon Sep 17 00:00:00 2001 From: Cuong Nguyen <cuongnb14@gmail.com> Date: Tue, 10 Oct 2023 08:57:35 +0700 Subject: [PATCH] add link for foreign key in display field --- admin_extended/base/__init__.py | 2 + .../base/custom_table_admin_page.py | 39 ++++++++++++++ .../{base.py => base/extended_admin_model.py} | 47 ++--------------- admin_extended/{ => base}/mixins.py | 52 ++++++++++++++++++- admin_extended/base/utils.py | 5 ++ 5 files changed, 101 insertions(+), 44 deletions(-) create mode 100644 admin_extended/base/__init__.py create mode 100644 admin_extended/base/custom_table_admin_page.py rename admin_extended/{base.py => base/extended_admin_model.py} (81%) rename admin_extended/{ => base}/mixins.py (74%) create mode 100644 admin_extended/base/utils.py diff --git a/admin_extended/base/__init__.py b/admin_extended/base/__init__.py new file mode 100644 index 0000000..b59eddb --- /dev/null +++ b/admin_extended/base/__init__.py @@ -0,0 +1,2 @@ +from .custom_table_admin_page import TableData, CustomTableAdminPage +from .extended_admin_model import ExtendedAdminModel \ No newline at end of file diff --git a/admin_extended/base/custom_table_admin_page.py b/admin_extended/base/custom_table_admin_page.py new file mode 100644 index 0000000..416a734 --- /dev/null +++ b/admin_extended/base/custom_table_admin_page.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass + +from django.contrib import admin +from django.shortcuts import render +from django.urls import path + + +@dataclass +class TableData: + header: str + table_titles = [] + table_rows = [] + + def add_rows(self, row: list): + self.table_rows.append(row) + + +class CustomTableAdminPage(admin.ModelAdmin): + model = None + + def get_urls(self): + view_name = '{}_{}_changelist'.format(self.model._meta.app_label, self.model._meta.model_name) + return [ + path('', self.custom_view, name=view_name), + ] + + def get_table_data(self): + """ + return list of TableData + """ + raise NotImplementedError() + + def custom_view(self, request, *args, **kwargs): + context = { + **admin.site.each_context(request), + 'tables': self.get_table_data(), + } + + return render(request, 'admin/custom/custom_table_page.html', context) diff --git a/admin_extended/base.py b/admin_extended/base/extended_admin_model.py similarity index 81% rename from admin_extended/base.py rename to admin_extended/base/extended_admin_model.py index c2c5c33..72ad68a 100644 --- a/admin_extended/base.py +++ b/admin_extended/base/extended_admin_model.py @@ -1,29 +1,13 @@ import copy -from dataclasses import dataclass from django.contrib import messages from django.contrib import admin -from django.shortcuts import render -from django.urls import path -from .mixins import UIUtilsMixin, ObjectToolModelAdminMixin -from .settings import ADMIN_EXTENDED_SETTINGS +from .mixins import UIUtilsMixin, ObjectToolModelAdminMixin, DispayLinkAdapter +from .utils import has_search_fields +from ..settings import ADMIN_EXTENDED_SETTINGS -def has_search_fields(field): - model_admin = admin.site._registry.get(field.related_model) - return model_admin and model_admin.search_fields - -@dataclass -class TableData: - header: str - table_titles = [] - table_rows = [] - - def add_rows(self, row: list): - self.table_rows.append(row) - - -class ExtendedAdminModel(ObjectToolModelAdminMixin, UIUtilsMixin, admin.ModelAdmin): +class ExtendedAdminModel(ObjectToolModelAdminMixin, UIUtilsMixin, DispayLinkAdapter, admin.ModelAdmin): """ Extend base model admin: tabbable inline model, separate view, edit model,... @@ -147,26 +131,3 @@ def get_inline_instances(self, request, obj=None): request.is_tabbed = self.tab_inline return super().get_inline_instances(request, obj) - -class CustomTableAdminPage(admin.ModelAdmin): - model = None - - def get_urls(self): - view_name = '{}_{}_changelist'.format(self.model._meta.app_label, self.model._meta.model_name) - return [ - path('', self.custom_view, name=view_name), - ] - - def get_table_data(self): - """ - return list of TableData - """ - raise NotImplementedError() - - def custom_view(self, request, *args, **kwargs): - context = { - **admin.site.each_context(request), - 'tables': self.get_table_data(), - } - - return render(request, 'admin/custom/custom_table_page.html', context) diff --git a/admin_extended/mixins.py b/admin_extended/base/mixins.py similarity index 74% rename from admin_extended/mixins.py rename to admin_extended/base/mixins.py index 93ed15a..e9d7a6c 100644 --- a/admin_extended/mixins.py +++ b/admin_extended/base/mixins.py @@ -1,6 +1,5 @@ import json from django.utils.html import format_html -from django.shortcuts import redirect from django.urls import path, reverse @@ -122,3 +121,54 @@ def changelist_view(self, request, extra_context=None): extra_context['change_list_object_tools'] = self._get_render_change_list_object_tools(request) return super().changelist_view(request, extra_context) + + +class DispayLinkAdapter: + + def _foreign_key_link(self, field_name, description): + """ + Converts a foreign key value into clickable links. + + If field_name is 'parent', link text will be str(obj.parent) + Link will be admin url for the admin url for obj.parent.id:change + """ + + def _display_fn(obj): + linked_obj = getattr(obj, field_name) + if linked_obj is None: + return '-' + app_label = linked_obj._meta.app_label + model_name = linked_obj._meta.model_name + view_name = f'admin:{app_label}_{model_name}_change' + link_url = reverse(view_name, args=[linked_obj.pk]) + return format_html('<a href="{}">{}</a>', link_url, linked_obj) + + _display_fn.short_description = description + return _display_fn + + + def convert_display_fields(self, list_display): + field_mapping = {} + for field in self.model._meta.fields: + + field_mapping[field.attname] = { + 'class_name': field.__class__.__name__, + 'verbose_name': field.verbose_name, + } + + if field.__class__.__name__ == 'ForeignKey': + field_mapping[field.attname[:-3]] = field_mapping[field.attname] # Eg `user_id` -> `user` key have same info + + results = [list_display[0]] + for field_name in list_display[1:]: # ignore first field + field_info = field_mapping.get(field_name) + if field_info and field_info['class_name'] == 'ForeignKey': + results.append(self._foreign_key_link(field_name, field_info['verbose_name'])) + else: + results.append(field_name) + + return results + + def get_list_display(self, request): + list_display = super().get_list_display(request) + return self.convert_display_fields(list_display) diff --git a/admin_extended/base/utils.py b/admin_extended/base/utils.py new file mode 100644 index 0000000..62cbac4 --- /dev/null +++ b/admin_extended/base/utils.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +def has_search_fields(field): + model_admin = admin.site._registry.get(field.related_model) + return model_admin and model_admin.search_fields