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

36,285

Solution 1

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)

Solution 2

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

Solution 3

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.

Solution 4

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.

Solution 5

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.

Share:
36,285
agf
Author by

agf

I answer Python questions on Stack Overflow. I also develop software at Braintree. For more information, visit http://www.adamforsyth.net or look me up on GitHub.

Updated on July 29, 2020

Comments

  • agf
    agf almost 4 years

    To deal with the lack of nested inlines in django-admin, I've put special cases into two of the templates to create links between the admin change pages and inline admins of two models.

    My question is: how do I create a link from the admin change page or inline admin of one model to the admin change page or inline admin of a related model cleanly, without nasty hacks in the template?

    I would like a general solution that I can apply to the admin change page or inline admin of any model.


    I have one model, post (not its real name) that is both an inline on the blog admin page, and also has its own admin page. The reason it can't just be inline is that it has models with foreign keys to it that only make sense when edited with it, and it only makes sense when edited with blog.

    For the post admin page, I changed part of "fieldset.html" from:

    {% if field.is_readonly %}
        <p>{{ field.contents }}</p>
    {% else %}
        {{ field.field }}
    {% endif %}
    

    to

    {% if field.is_readonly %}
        <p>{{ field.contents }}</p>
    {% else %}
        {% ifequal field.field.name "blog" %}
            <p>{{ field.field.form.instance.blog_link|safe }}</p>
        {% else %}
            {{ field.field }}
        {% endifequal %}
    {% endif %}
    

    to create a link to the blog admin page, where blog_link is a method on the model:

    def blog_link(self):
          return '<a href="%s">%s</a>' % (reverse("admin:myblog_blog_change",  
                                            args=(self.blog.id,)), escape(self.blog))
    

    I couldn't find the id of the blog instance anywhere outside field.field.form.instance.

    On the blog admin page, where post is inline, I modified part of "stacked.html" from:

    <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;
    <span class="inline_label">{% if inline_admin_form.original %}
        {{ inline_admin_form.original }}
    {% else %}#{{ forloop.counter }}{% endif %}</span>
    

    to

    <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;
    <span class="inline_label">{% if inline_admin_form.original %}
        {% ifequal inline_admin_formset.opts.verbose_name "post" %}
        <a href="/admin/myblog/post/{{ inline_admin_form.pk_field.field.value }}/">
                {{ inline_admin_form.original }}</a>
    {% else %}{{ inline_admin_form.original }}{% endifequal %}
    {% else %}#{{ forloop.counter }}{% endif %}</span>
    

    to create a link to the post admin page since here I was able to find the id stored in the foreign key field.


    I'm sure there is a better, more general way to do add links to admin forms without repeating myself; what is it?