[Django]-How do I add a link from the Django admin page of one object to the admin page of a related object?

17👍

New in Django 1.8 : show_change_link for inline admin.

Set show_change_link to True (False by default) in your inline model, so that inline objects have a link to their change form (where they can have their own inlines).

from django.contrib import admin

class PostInline(admin.StackedInline):
    model = Post
    show_change_link = True
    ...

class BlogAdmin(admin.ModelAdmin):
    inlines = [PostInline]
    ...

class ImageInline(admin.StackedInline):
    # Assume Image model has foreign key to Post
    model = Image
    show_change_link = True
    ...

class PostAdmin(admin.ModelAdmin):
    inlines = [ImageInline]
    ...

admin.site.register(Blog, BlogAdmin)
admin.site.register(Post, PostAdmin)
👤bitnik

19👍

Use readonly_fields:

class MyInline(admin.TabularInline):
    model = MyModel
    readonly_fields = ['link']

    def link(self, obj):
        url = reverse(...)
        return mark_safe("<a href='%s'>edit</a>" % url)

    # the following is necessary if 'link' method is also used in list_display
    link.allow_tags = True

12👍

This is my current solution, based on what was suggested by Pannu (in his edit) and Mikhail.

I have a couple of top-level admin change view I need to link to a top-level admin change view of a related object, and a couple of inline admin change views I need to link to the top-level admin change view of the same object. Because of that, I want to factor out the link method rather than repeating variations of it for every admin change view.

I use a class decorator to create the link callable, and add it to readonly_fields.

def add_link_field(target_model = None, field = '', link_text = unicode):
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()
        def link(self, instance):
            app_name = instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance
            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = reverse_name + ' link'
        cls.link = link
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + ['link']
        return cls
    return add_link

You can also pass a custom callable if you need to get your link text in some way than just calling unicode on the object you’re linking to.

I use it like this:

# the first 'blog' is the name of the model who's change page you want to link to
# the second is the name of the field on the model you're linking from
# so here, Post.blog is a foreign key to a Blog object. 
@add_link_field('blog', 'blog')
class PostAdmin(admin.ModelAdmin):
    inlines = [SubPostInline, DefinitionInline]
    fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
    list_display = ('__unicode__', 'enabled', 'link')

# can call without arguments when you want to link to the model change page
# for the model of an inline model admin.
@add_link_field()
class PostInline(admin.StackedInline):
    model = Post
    fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
    extra = 0

Of course none of this would be necessary if I could nest the admin change views for SubPost and Definition inside the inline admin of Post on the Blog admin change page without patching Django.

👤agf

11👍

I think that agf’s solution is pretty awesome — lots of kudos to him. But I needed a couple more features:

  • to be able to have multiple links for one admin
  • to be able to link to model in different app

Solution:

def add_link_field(target_model = None, field = '', app='', field_name='link',
                   link_text=unicode):
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()
        def link(self, instance):
            app_name = app or instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance
            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = reverse_name + ' link'
        setattr(cls, field_name, link)
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
            [field_name]
        return cls
    return add_link

Usage:

# 'apple' is name of model to link to
# 'fruit_food' is field name in `instance`, so instance.fruit_food = Apple()
# 'link2' will be name of this field
@add_link_field('apple','fruit_food',field_name='link2')
# 'cheese' is name of model to link to
# 'milk_food' is field name in `instance`, so instance.milk_food = Cheese()
# 'milk' is the name of the app where Cheese lives
@add_link_field('cheese','milk_food', 'milk')
class FoodAdmin(admin.ModelAdmin):
    list_display = ("id", "...", 'link', 'link2')

I am sorry that the example is so illogical, but I didn’t want to use my data.

4👍

I agree that its hard to do template editing so, I create a custom widget to show an anchor on the admin change view page(can be used on both forms and inline forms).

So, I used the anchor widget, along with form overriding to get the link on the page.

forms.py:

class AnchorWidget(forms.Widget):

    def _format_value(self,value):
        if self.is_localized:
            return formats.localize_input(value)
        return value

    def render(self, name, value, attrs=None):
        if not value:
            value = u''

        text = unicode("")
        if self.attrs.has_key('text'):
            text = self.attrs.pop('text')

        final_attrs = self.build_attrs(attrs,name=name)

        return mark_safe(u"<a %s>%s</a>" %(flatatt(final_attrs),unicode(text)))

class PostAdminForm(forms.ModelForm):
    .......

    def __init__(self,*args,**kwargs):
        super(PostAdminForm, self).__init__(*args, **kwargs)
        instance = kwargs.get('instance',None)
        if instance.blog:
            href = reverse("admin:appname_Blog_change",args=(instance.blog))  
            self.fields["link"] = forms.CharField(label="View Blog",required=False,widget=AnchorWidget(attrs={'text':'go to blog','href':href}))


 class BlogAdminForm(forms.ModelForm):
    .......
    link = forms..CharField(label="View Post",required=False,widget=AnchorWidget(attrs={'text':'go to post'}))

    def __init__(self,*args,**kwargs):
        super(BlogAdminForm, self).__init__(*args, **kwargs)
        instance = kwargs.get('instance',None)
        href = ""
        if instance:
            posts = Post.objects.filter(blog=instance.pk)
            for idx,post in enumerate(posts):
                href = reverse("admin:appname_Post_change",args=(post["id"]))  
                self.fields["link_%s" % idx] = forms..CharField(label=Post["name"],required=False,widget=AnchorWidget(attrs={'text':post["desc"],'href':href}))

now in your ModelAdmin override the form attribute and you should get the desired result. I assumed you have a OneToOne relationship between these tables, If you have one to many then the BlogAdmin side will not work.

update:
I’ve made some changes to dynamically add links and that also solves the OneToMany issue with the Blog to Post hope this solves the issue. 🙂

After Pastebin:
In Your PostAdmin I noticed blog_link, that means your trying to show the blog link on changelist_view which lists all the posts. If I’m correct then you should add a method to show the link on the page.

class PostAdmin(admin.ModelAdmin):
    model = Post
    inlines = [SubPostInline, DefinitionInline]
    list_display = ('__unicode__', 'enabled', 'blog_on_site')

    def blog_on_site(self, obj):
        href = reverse("admin:appname_Blog_change",args=(obj.blog))
        return mark_safe(u"<a href='%s'>%s</a>" %(href,obj.desc))
    blog_on_site.allow_tags = True
    blog_on_site.short_description = 'Blog'

As far as the showing post links on BlogAdmin changelist_view you can do the same as above. My earlier solution will show you the link one level lower at the change_view page where you can edit each instance.

If you want the BlogAdmin page to show the links to the post in the change_view page then you will have to include each in the fieldsets dynamically by overriding the get_form method for class BlogAdmin and adding the link’s dynamically, in get_form set the self.fieldsets, but first don’t use tuples to for fieldsets instead use a list.

👤Pannu

2👍

Based on agfs and SummerBreeze’s suggestions, I’ve improved the decorator to handle unicode better and to be able to link to backwards-foreignkey fields (ManyRelatedManager with one result). Also you can now add a short_description as a list header:

from django.core.urlresolvers import reverse
from django.core.exceptions import MultipleObjectsReturned
from django.utils.safestring import mark_safe

def add_link_field(target_model=None, field='', app='', field_name='link',
                   link_text=unicode, short_description=None):
    """
    decorator that automatically links to a model instance in the admin;
    inspired by http://stackoverflow.com/questions/9919780/how-do-i-add-a-link-from-the-django-admin-page-of-one-object-
    to-the-admin-page-o
    :param target_model: modelname.lower or model
    :param field: fieldname
    :param app: appname
    :param field_name: resulting field name
    :param link_text: callback to link text function
    :param short_description: list header
    :return:
    """
    def add_link(cls):
        reverse_name = target_model or cls.model.__name__.lower()

        def link(self, instance):
            app_name = app or instance._meta.app_label
            reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
            link_obj = getattr(instance, field, None) or instance

            # manyrelatedmanager with one result?
            if link_obj.__class__.__name__ == "RelatedManager":
                try:
                    link_obj = link_obj.get()
                except MultipleObjectsReturned:
                    return u"multiple, can't link"
                except link_obj.model.DoesNotExist:
                    return u""

            url = reverse(reverse_path, args = (link_obj.id,))
            return mark_safe(u"<a href='%s'>%s</a>" % (url, link_text(link_obj)))
        link.allow_tags = True
        link.short_description = short_description or (reverse_name + ' link')
        setattr(cls, field_name, link)
        cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
            [field_name]
        return cls
    return add_link

Edit: updated due to link being gone.

👤panni

0👍

Looking through the source of the admin classes is enlightening: it shows that there is an object in context available to an admin view called “original”.

Here is a similar situation, where I needed some info added to a change list view: Adding data to admin templates (on my blog).

Leave a comment