47👍
The solution below uses this answer
but makes it reusable by all models, avoiding the need to add methods to each admin class.
Example Models
# models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=200)
population = models.IntegerField()
class Career(models.Model):
name = models.CharField(max_length=200)
average_salary = models.IntegerField()
class Person(models.Model):
name = models.CharField(max_length=200)
age = models.IntegerField()
country = models.ForeignKey(Country, on_delete=models.CASCADE)
career = models.ForeignKey(Career, on_delete=models.CASCADE)
Example Admin
# admin.py
from django.utils.html import format_html
from django.urls import reverse
from .models import Person
def linkify(field_name):
"""
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 _linkify(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)
_linkify.short_description = field_name # Sets column name
return _linkify
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
list_display = [
"name",
"age",
linkify(field_name="country"),
linkify(field_name="career"),
]
Results
Given an App named app
, and a Person instance Person(name='Adam' age=20)
with country and carreer foreign key values with ids 123
and 456
,
the list result will be:
| Name | Age | Country |...|
|------|-----|-----------------------------------------------------------|...|
| Adam | 20 | <a href="/admin/app/country/123">Country object(123)</a> |...|
(Continues)
|...| Career |
|---|---------------------------------------------------------|
|...| <a href="/admin/app/career/456">Career object(456)</a> |
1👍
A good start would be looking at the source of BaseModelAdmin
and ModelAdmin
. Try to find out how the ModelAdmin generates the default links.
Extend ModelAdmin
, add a method to generate links to arbitrary foreign keys and look at how ChangeList
generates the change list.
I would also suggest you use format_html to render the links, which makes link_to_bar.allow_tags = True
unnecessary:
from django.utils.html import format_html
class FooAdmin(ModelAdmin):
list_display = ('link_to_bar', )
def link_to_bar(self, obj):
link = urlresolvers.reverse('admin:app_bar_change', args=[obj.bar_id])
return format_html('<a href="{}">{}</a>', link, obj.bar) if obj.bar else None
- Why is django's settings object a LazyObject?
- How can i get all models in django 1.8
- Django vs. Pylons
- How can I test if my redis cache is working?
- Matplotlib: interactive plot on a web server
1👍
A slight respin on the accepted answer. It is not necessarily better, but implements some of the advice in the comments:
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.html import format_html
def linkify(field_name):
def _linkify(obj):
content_type = ContentType.objects.get_for_model(obj)
app_label = content_type.app_label
linked_obj = getattr(obj, field_name)
linked_content_type = ContentType.objects.get_for_model(linked_obj)
model_name = linked_content_type.model
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)
_linkify.short_description = field_name.replace("_", " ").capitalize()
return _linkify
- Session authentication with Django channels
- Improving Performance of Django ForeignKey Fields in Admin
- Django with NoSQL database
- Django + Forms: Dynamic choices for ChoiceField
0👍
This is an improvement using this answer, allowing you to supply an optional alternative short description instead of the supplied field_name
being used for the column header. And also an optional label_prop
value to display an alternative property value of the model if so desired
EDIT: I’ve also now updated it to handle ManyToManyFields
from typing import Optional
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from django.db.models import ManyToManyField, ForeignKey, Model
from django.urls import reverse
from django.utils.html import format_html
def create_link(linked_obj, app_label: str, label_prop: Optional[str] = None) -> str:
linked_content_type = ContentType.objects.get_for_model(linked_obj)
model_name = linked_content_type.model
view_name = f"admin:{app_label}_{model_name}_change"
link_url = reverse(view_name, args=[linked_obj.pk])
return "<a href='%s'>%s</a>" % (link_url, getattr(linked_obj, label_prop) if label_prop else linked_obj)
def linkify(field_name: str, label_prop: Optional[str] = None, short_description: Optional[str] = None, as_html_list: bool = False):
def _linkify(obj: Model):
content_type = ContentType.objects.get_for_model(obj)
app_label = content_type.app_label
field_type = obj._meta.get_field(field_name)
items = None
if isinstance(field_type, ManyToManyField):
items = list(getattr(obj, field_name).all())
elif isinstance(field_type, ForeignKey):
items = [getattr(obj, field_name)]
else:
print(f'field_name {field_name} is not ManyToManyField or ForeignKey')
links = [create_link(itm, app_label, label_prop) for itm in items if itm is not None]
if len(links) > 1:
if as_html_list:
html = "<ul>"
for link in links:
html += f'<li>{link}</li>'
html += "</ul>"
else:
html = ", ".join(links)
else:
html = links[0]
return format_html(html)
_linkify.short_description = [short_description, field_name.replace("_", " ").capitalize()][short_description is None]
return _linkify
Example usage:
Models
# models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=200)
population = models.IntegerField()
class Career(models.Model):
name = models.CharField(max_length=200)
average_salary = models.IntegerField()
class Person(models.Model):
name = models.CharField(max_length=200)
age = models.IntegerField()
country = models.ForeignKey(Country, on_delete=models.CASCADE)
career = models.ForeignKey(Career, on_delete=models.CASCADE)
Admin
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
list_display = [
"name",
"age",
linkify(
field_name="country",
label_prop="name",
short_description="Country Name"
),
linkify(field_name="career"),
]
Result
Given the name
of Adam’s country is France.
| Name | Age | Country Name |...|
|------|-----|----------------------------------------------------------|...|
| Adam | 20 | <a href="/admin/app/country/123">France</a> |...|
It’s also worth noting…
…the linkify
function can also be used along side the display decorator
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
list_display = [
"name",
"age",
"sortable_country_link",
linkify(field_name="career"),
]
@admin.display(description='Country Name', ordering='country__name')
def sortable_country_link(self, obj):
return linkify(field_name='country', label_prop='name')(obj)