60

My app has users who create pages. In the Page screen of the admin, I'd like to list the User who created the page, and in that list, I'd like the username to have a link that goes to the user page in admin (not the Page).

class PageAdmin(admin.ModelAdmin):
    list_display = ('name', 'user', )
    list_display_links = ('name','user',)
admin.site.register(Page, PageAdmin)

I was hoping that by making it a link in the list_display it would default to link to the actual user object, but it still goes to Page.

I'm sure I'm missing something simple here.

djvg
  • 11,722
  • 5
  • 72
  • 103
Brenden
  • 8,264
  • 14
  • 48
  • 78

7 Answers7

89

Modifying your model isn't necessary, and it's actually a bad practice (adding admin-specific view-logic into your models? Yuck!) It may not even be possible in some scenarios.

Luckily, it can all be achieved from the ModelAdmin class:

from django.urls import reverse
from django.utils.safestring import mark_safe    


class PageAdmin(admin.ModelAdmin):
    # Add it to the list view:
    list_display = ('name', 'user_link', )
    # Add it to the details view:
    readonly_fields = ('user_link',)

    def user_link(self, obj):
        return mark_safe('<a href="{}">{}</a>'.format(
            reverse("admin:auth_user_change", args=(obj.user.pk,)),
            obj.user.email
        ))
    user_link.short_description = 'user'


admin.site.register(Page, PageAdmin)

Edit 2016-01-17: Updated answer to use make_safe, since allow_tags is now deprecated.

Edit 2019-06-14: Updated answer to use django.urls, since as of Django 1.10 django.core.urls has been deprecated.

Matt
  • 5,028
  • 2
  • 28
  • 55
WhyNotHugo
  • 9,423
  • 6
  • 62
  • 70
25

Add this to your model:

  def user_link(self):
      return '<a href="%s">%s</a>' % (reverse("admin:auth_user_change", args=(self.user.id,)) , escape(self.user))

  user_link.allow_tags = True
  user_link.short_description = "User" 

You might also need to add the following to the top of models.py:

  from django.template.defaultfilters import escape
  from django.core.urls import reverse

In admin.py, in list_display, add user_link:

list_display = ('name', 'user_link', )

No need for list_display_links.

Community
  • 1
  • 1
Udi
  • 29,222
  • 9
  • 96
  • 129
18

You need to use format_html for modern versions of django

@admin.register(models.Foo)
class FooAdmin(admin.ModelAdmin):
    list_display = ('ts', 'bar_link',)

    def bar_link(self, item):
        from django.shortcuts import resolve_url
        from django.contrib.admin.templatetags.admin_urls import admin_urlname
        url = resolve_url(admin_urlname(models.Bar._meta, 'change'), item.bar.id)
        return format_html(
            '<a href="{url}">{name}</a>'.format(url=url, name=str(item.bar))
        )
djvg
  • 11,722
  • 5
  • 72
  • 103
Janusz Skonieczny
  • 17,642
  • 11
  • 55
  • 63
  • 3
    Don't forget to also add " from django.utils.html import format_html " – Andre Miller May 25 '19 at 19:35
  • By calling `'...'.format()` inside `format_html()`, you are actually bypassing the escaping mechanism... It should be: `format_html('{}', url, item.bar)` (replaced kwargs by args just for brevity) – djvg Jul 26 '22 at 15:59
13

I ended up with a simple helper:

from django.shortcuts import resolve_url
from django.utils.safestring import SafeText
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.utils.html import format_html


def model_admin_url(obj: Model, name: str = None) -> str:
    url = resolve_url(admin_urlname(obj._meta, SafeText("change")), obj.pk)
    return format_html('<a href="{}">{}</a>', url, name or str(obj))

Then you can use the helper in your model-admin:

class MyAdmin(admin.ModelAdmin):
    readonly_field = ["my_link"]

    def my_link(self, obj):
        return model_admin_url(obj.my_foreign_key)
imbolc
  • 1,620
  • 1
  • 19
  • 32
8

I needed this for a lot of my admin pages, so I created a mixin for it that handles different use cases:

pip install django-admin-relation-links

Then:

from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin


@admin.register(Group)
class MyModelAdmin(AdminChangeLinksMixin, admin.ModelAdmin):

    # ...

    change_links = ['field_name']

See the GitHub page for more info. Try it out and let me know how it works out!

https://github.com/gitaarik/django-admin-relation-links

gitaarik
  • 42,736
  • 12
  • 98
  • 105
  • 1
    I tried it and it did not work for me, it may be because of the documentation. – dsax7 Jul 27 '17 at 18:43
  • 1
    @filtfilt can you please open an issue on the GitHub page describing your problem? – gitaarik Jul 28 '17 at 12:29
  • 1
    @filtfilt it was maybe because of the wrong ordering of the inheritance, you should put `AdminChangeLinksMixin` first. It was wrong in the readme, updated that now. – gitaarik Jul 28 '17 at 12:35
  • 1
    I added `change_links = ['model_name']`, but it still does not work :(. I get ´model_name: - ´ in the inlines, but nothing more. It may be because the model, which I have my `OneToOneField` to, is in a different application. – dsax7 Jul 28 '17 at 14:32
  • 1
    @filtfilt please open an issue on the GitHub page, no need to pollute this answer here on SO ;) – gitaarik Jul 28 '17 at 14:36
  • Doesn't work for me either. Am adding it here so SO users can consider whether or not to try this. I had a look at your documentation. It is not clear on how to use. Please clarify how to use. – Harlin Aug 06 '21 at 16:33
  • @Harlin Please open an issue on the GitHub project to get help. SO isn't the place to discuss these things. – gitaarik Aug 12 '21 at 10:45
5

I decided to make a simple admin mixin that looks like this (see docstring for usage):

from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from rest_framework.reverse import reverse


class RelatedObjectLinkMixin(object):
    """    
    Generate links to related links. Add this mixin to a Django admin model. Add a 'link_fields' attribute to the admin
    containing a list of related model fields and then add the attribute name with a '_link' suffix to the
    list_display attribute. For Example a Student model with a 'teacher' attribute would have an Admin class like this:

    class StudentAdmin(RelatedObjectLinkMixin, ...):
        link_fields = ['teacher']

        list_display = [
            ...
            'teacher_link'
            ...
        ]
    """

    link_fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.link_fields:
            for field_name in self.link_fields:
                func_name = field_name + '_link'
                setattr(self, func_name, self._generate_link_func(field_name))

    def _generate_link_func(self, field_name):
        def _func(obj, *args, **kwargs):
            related_obj = getattr(obj, field_name)
            if related_obj:
                content_type = ContentType.objects.get_for_model(related_obj.__class__)
                url_name = 'admin:%s_%s_change' % (content_type.app_label, content_type.model)
                url = reverse(url_name, args=[related_obj.pk])
                return format_html('<a href="{}" class="changelink">{}</a>', url, str(related_obj))
            else:
                return None
        return _func
MikkoP
  • 636
  • 1
  • 7
  • 17
  • 1
    You really don't need `ContentType` for this; you could just use `obj._meta.model` (or `obj.__class__`). – WhyNotHugo Feb 10 '18 at 00:28
  • 1
    Stick `_func.short_description = field_name` before `return _func` otherwise all your columns will have the header "FUNC". This preserves the original name. – wjdp Jan 27 '19 at 03:11
  • 1
    Here is an updated mixin that is working on Django 2.2 and includes the above recommendations https://gist.github.com/Vigrond/ac3c468377ce6d3e53f9b7059fd42569 – Vigrond Jun 11 '19 at 04:19
2

If anyone is trying to do this with inline admin, consider a property called show_change_link since Django 1.8.

Your code could then look like this:

class QuestionInline(admin.TabularInline):
    model = Question
    extra = 1
    show_change_link = True


class TestAdmin(admin.ModelAdmin):
    inlines = (QuestionInline,)


admin.site.register(Test, TestAdmin)

This will add a change/update link for each foreign key relationship in the admin's inline section.

Harlin
  • 1,059
  • 14
  • 18